理解简单模型中的 Tree SHAP

一个特征的 SHAP 值,是在所有可能的特征排序中,逐一引入特征时,以该特征为条件,模型输出的平均变化。虽然这个陈述很简单,但计算起来却很有挑战性。因此,本笔记旨在提供一些简单的示例,以便我们了解在非常小的树中这是如何运作的。对于任意大的树,通过观察树来直观地猜测这些值是非常困难的。

[1]:
import graphviz
import numpy as np
import pandas as pd
from sklearn.tree import DecisionTreeRegressor, export_graphviz

import shap

单次分裂示例

[2]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: N // 2, 0] = 1
y[: N // 2] = 1

# fit model
single_split_model = DecisionTreeRegressor(max_depth=1)
single_split_model.fit(X, y)

# draw model
dot_data = export_graphviz(
    single_split_model,
    out_file=None,
    filled=True,
    rounded=True,
    special_characters=True,
)
graph = graphviz.Source(dot_data)
graph
[2]:
../../../_images/example_notebooks_tabular_examples_tree_based_models_Understanding_Tree_SHAP_for_Simple_Models_3_0.svg

解释模型

请注意,偏置项是模型在训练数据集上的期望输出(0.5)。模型中未使用的特征的 SHAP 值始终为 0,而对于 \(x_0\),其 SHAP 值就是期望值与模型输出之间的差值。

[3]:
xs = [np.ones(M), np.zeros(M)]
df = pd.DataFrame()
for idx, x in enumerate(xs):
    index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
    df = pd.concat(
        [
            df,
            pd.DataFrame(
                [x, shap.TreeExplainer(single_split_model).shap_values(x)],
                index=index,
                columns=["x0", "x1", "x2", "x3"],
            ),
        ]
    )
df
[3]:
x0 x1 x2 x3
示例 0 x 1.0 1.0 1.0 1.0
shap_values 0.5 0.0 0.0 0.0
示例 1 x 0.0 0.0 0.0 0.0
shap_values -0.5 0.0 0.0 0.0

双特征 AND 示例

我们在此示例中使用两个特征。如果特征 \(x_{0} = 1\) 并且 \(x_{1} = 1\),则目标值为 1,否则为 0。因此,我们称之为 AND 模型。

[4]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: 1 * N // 4, 1] = 1
X[: N // 2, 0] = 1
X[N // 2 : 3 * N // 4, 1] = 1
y[: 1 * N // 4] = 1

# fit model
and_model = DecisionTreeRegressor(max_depth=2)
and_model.fit(X, y)

# draw model
dot_data = export_graphviz(and_model, out_file=None, filled=True, rounded=True, special_characters=True)
graph = graphviz.Source(dot_data)
graph
[4]:
../../../_images/example_notebooks_tabular_examples_tree_based_models_Understanding_Tree_SHAP_for_Simple_Models_8_0.svg

解释模型

请注意,偏置项是模型在训练数据集上的期望输出(0.25)。未使用的特征 \(x_2\)\(x_3\) 的 SHAP 值始终为 0。对于 \(x_0\)\(x_1\),其 SHAP 值就是期望值(0.25)与模型输出之间的差值在它们之间平均分配(因为它们对 AND 函数的贡献相同)。

[5]:
xs = np.array([np.ones(M), np.zeros(M)])
# np.array([np.ones(M), np.zeros(M), np.array([1, 0, 1, 0]), np.array([0, 1, 0, 0])]   # you can also check these examples
df = pd.DataFrame()
for idx, x in enumerate(xs):
    index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
    df = pd.concat(
        [
            df,
            pd.DataFrame(
                [x, shap.TreeExplainer(and_model).shap_values(x)],
                index=index,
                columns=["x0", "x1", "x2", "x3"],
            ),
        ]
    )
df
[5]:
x0 x1 x2 x3
示例 0 x 1.000 1.000 1.0 1.0
shap_values 0.375 0.375 0.0 0.0
示例 1 x 0.000 0.000 0.0 0.0
shap_values -0.125 -0.125 0.0 0.0
[6]:
y.mean()
[6]:
np.float64(0.25)

以下是示例 1 的 Shap 值的计算过程:偏置项 (y.mean()) 是 0.25,目标值是 1。这剩下 1 - 0.25 = 0.75 需要在相关特征之间分配。由于只有 \(x_0\)\(x_1\) 对目标值有贡献(且贡献程度相同),因此将 0.75 在它们之间平分,即每个特征分得 0.375。

双特征 OR 示例

我们对上面的示例做一个小小的改动。如果 \(x_{0} = 1\)\(x_{1} = 1\),则目标值为 1,否则为 0。您能在不向下滚动的情况下猜出 SHAP 值吗?

[7]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: N // 2, 0] = 1
X[: 1 * N // 4, 1] = 1
X[N // 2 : 3 * N // 4, 1] = 1
y[: N // 2] = 1
y[N // 2 : 3 * N // 4] = 1

# fit model
or_model = DecisionTreeRegressor(max_depth=2)
or_model.fit(X, y)

# draw model
dot_data = export_graphviz(or_model, out_file=None, filled=True, rounded=True, special_characters=True)
graph = graphviz.Source(dot_data)
graph
[7]:
../../../_images/example_notebooks_tabular_examples_tree_based_models_Understanding_Tree_SHAP_for_Simple_Models_15_0.svg

解释模型

请注意,偏置项是模型在训练数据集上的期望输出(0.75)。模型中未使用的特征的 SHAP 值始终为 0,而对于 \(x_0\)\(x_1\),其 SHAP 值就是期望值与模型输出之间的差值在它们之间平均分配(因为它们对 OR 函数的贡献相同)。

[8]:
xs = np.array([np.ones(M), np.zeros(M)])
# np.array([np.ones(M), np.zeros(M), np.array([1, 0, 1, 0]), np.array([0, 1, 0, 0])]   # you can also check these examples
df = pd.DataFrame()
for idx, x in enumerate(xs):
    index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
    df = pd.concat(
        [
            df,
            pd.DataFrame(
                [x, shap.TreeExplainer(or_model).shap_values(x)],
                index=index,
                columns=["x0", "x1", "x2", "x3"],
            ),
        ]
    )
df
[8]:
x0 x1 x2 x3
示例 0 x 1.000 1.000 1.0 1.0
shap_values 0.125 0.125 0.0 0.0
示例 1 x 0.000 0.000 0.0 0.0
shap_values -0.375 -0.375 0.0 0.0

双特征 XOR 示例

[9]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: N // 2, 0] = 1
X[: 1 * N // 4, 1] = 1
X[N // 2 : 3 * N // 4, 1] = 1
y[1 * N // 4 : N // 2] = 1
y[N // 2 : 3 * N // 4] = 1

# fit model
xor_model = DecisionTreeRegressor(max_depth=2)
xor_model.fit(X, y)

# draw model
dot_data = export_graphviz(xor_model, out_file=None, filled=True, rounded=True, special_characters=True)
graph = graphviz.Source(dot_data)
graph
[9]:
../../../_images/example_notebooks_tabular_examples_tree_based_models_Understanding_Tree_SHAP_for_Simple_Models_19_0.svg

解释模型

请注意,偏置项是模型在训练数据集上的期望输出(0.5)。模型中未使用的特征的 SHAP 值始终为 0,而对于 \(x_0\)\(x_1\),其 SHAP 值就是期望值与模型输出之间的差值在它们之间平均分配(因为它们对 XOR 函数的贡献相同)。

[10]:
xs = np.array([np.ones(M), np.zeros(M)])
# np.array([np.ones(M), np.zeros(M), np.array([1, 0, 1, 0]), np.array([0, 1, 0, 0])]   # you can also check these examples
df = pd.DataFrame()
for idx, x in enumerate(xs):
    index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
    df = pd.concat(
        [
            df,
            pd.DataFrame(
                [x, shap.TreeExplainer(xor_model).shap_values(x)],
                index=index,
                columns=["x0", "x1", "x2", "x3"],
            ),
        ]
    )
df
[10]:
x0 x1 x2 x3
示例 0 x 1.00 1.00 1.0 1.0
shap_values -0.25 -0.25 0.0 0.0
示例 1 x 0.00 0.00 0.0 0.0
shap_values -0.25 -0.25 0.0 0.0

双特征 AND + 特征提升示例

[11]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: N // 2, 0] = 1
X[: 1 * N // 4, 1] = 1
X[N // 2 : 3 * N // 4, 1] = 1
y[: 1 * N // 4] = 1
y[: N // 2] += 1

# fit model
and_fb_model = DecisionTreeRegressor(max_depth=2)
and_fb_model.fit(X, y)

# draw model
dot_data = export_graphviz(and_fb_model, out_file=None, filled=True, rounded=True, special_characters=True)
graph = graphviz.Source(dot_data)
graph
[11]:
../../../_images/example_notebooks_tabular_examples_tree_based_models_Understanding_Tree_SHAP_for_Simple_Models_23_0.svg

解释模型

请注意,偏置项是模型在训练数据集上的期望输出(0.75)。模型中未使用的特征的 SHAP 值始终为 0,而对于 \(x_0\)\(x_1\),其 SHAP 值是期望值与模型输出之间的差值在它们之间平均分配(因为它们对 AND 函数的贡献相同),再加上 \(x_0\) 的额外 0.5 影响,因为它本身就具有 \(1.0\) 的影响(如果它为 on,则为 +0.5;如果为 off,则为 -0.5)。

[12]:
xs = np.array([np.ones(M), np.zeros(M)])
# np.array([np.ones(M), np.zeros(M), np.array([1, 0, 1, 0]), np.array([0, 1, 0, 0])]   # you can also check these examples
df = pd.DataFrame()
for idx, x in enumerate(xs):
    index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
    df = pd.concat(
        [
            df,
            pd.DataFrame(
                [x, shap.TreeExplainer(and_fb_model).shap_values(x)],
                index=index,
                columns=["x0", "x1", "x2", "x3"],
            ),
        ]
    )
df
[12]:
x0 x1 x2 x3
示例 0 x 1.000 1.000 1.0 1.0
shap_values 0.875 0.375 0.0 0.0
示例 1 x 0.000 0.000 0.0 0.0
shap_values -0.625 -0.125 0.0 0.0